/*
 * Copyright (C) 2014 Miquel Sas
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package traderlib.core.data.entity;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import traderlib.core.data.types.Types;
import traderlib.core.data.types.Value;

/**
 * A condition used to build filtering criteria.
 *
 * @author Miquel Sas
 */
public class Condition {

	/**
	 * Enumeration constants of Operators.
	 */
	public static enum Operator {

		/**
		 * Starts with, in SQL <code>FIELD LIKE '....&#37;'</code>
		 */
		LIKE_LEFT(1),
		/**
		 * Contains, in SQL <code>FIELD LIKE '&#37;....&#37;'</code>
		 */
		LIKE_MID(1),
		/**
		 * Ends with, in SQL <code>FIELD LIKE '&#37;....'</code>
		 */
		LIKE_RIGHT(1),
		/**
		 * Equals, in SQL <code>FIELD = &#63;</code>
		 */
		FIELD_EQ(1),
		/**
		 * Greater than, in SQL <code>FIELD &gt; &#63;</code>
		 */
		FIELD_GT(1),
		/**
		 * Greater than or equal to, in SQL <code>FIELD &gt;= &#63;</code>
		 */
		FIELD_GE(1),
		/**
		 * Less than, in SQL <code>FIELD &lt; &#63;</code>
		 */
		FIELD_LT(1),
		/**
		 * Less than or equal to, in SQL <code>FIELD &lt;= &#63;</code>
		 */
		FIELD_LE(1),
		/**
		 * Not equal to, in SQL <code>FIELD != &#63;</code>
		 */
		FIELD_NE(1),
		/**
		 * In the list, in SQL <code>FIELD IN (&#63;,&#63;,...)</code>
		 */
		IN_LIST(-1),
		/**
		 * Is null, in SQL <code>FIELD IS NULL</code>
		 */
		IS_NULL(0),
		/**
		 * Between, in SQL <code>FIELD BETWEEN &#63; AND &#63;</code>
		 */
		BETWEEN(2),
		/**
		 * Does not start with, in SQL <code>FIELD NOT LIKE '....&#37;'</code>
		 */
		NOT_LIKE_LEFT(1),
		/**
		 * Does not contains, in SQL <code>FIELD NOT LIKE '&#37;....&#37;'</code>
		 */
		NOT_LIKE_MID(1),
		/**
		 * Does not end with, in SQL <code>FIELD NOT LIKE '&#37;....'</code>
		 */
		NOT_LIKE_RIGHT(1),
		/**
		 * Not in the list, in SQL <code>FIELD NOT IN (&#63;,&#63;,...)</code>
		 */
		NOT_IN_LIST(-1),
		/**
		 * Not null, in SQL <code>FIELD IS NOT NULL</code>
		 */
		NOT_IS_NULL(0),
		/**
		 * Not between, in SQL <code>FIELD NOT BETWEEN &#63; AND &#63;</code>
		 */
		NOT_BETWEEN(2),
		/**
		 * Starts with, no case, in SQL <code>UPPER(FIELD) LIKE '....&#37;'</code>
		 */
		LIKE_LEFT_NOCASE(1),
		/**
		 * Contains no case, in SQL <code>UPPER(FIELD) LIKE '&#37;....&#37;'</code>
		 */
		LIKE_MID_NOCASE(1),
		/**
		 * Ends with no case, in SQL <code>UPPER(FIELD) LIKE '&#37;....'</code>
		 */
		LIKE_RIGHT_NOCASE(1),
		/**
		 * Equals no case, in SQL <code>UPPER(FIELD) = UPPER(&#63;)</code>
		 */
		FIELD_EQ_NOCASE(1),
		/**
		 * Greater than no case, in SQL <code>UPPER(FIELD) &gt; UPPER(&#63;)</code>
		 */
		FIELD_GT_NOCASE(1),
		/**
		 * Greater than or equal to no case, in SQL <code>UPPER(FIELD) &gt;= UPPER(&#63;)</code>
		 */
		FIELD_GE_NOCASE(1),
		/**
		 * Less than no case, in SQL <code>UPPER(FIELD) &lt; UPPER(&#63;)</code>
		 */
		FIELD_LT_NOCASE(1),
		/**
		 * Less than or equal to no case, in SQL <code>UPPER(FIELD) &lt;= UPPER(&#63;)</code>
		 */
		FIELD_LE_NOCASE(1),
		/**
		 * Not equal no case <code>UPPER(FIELD) != UPPER(&#63;)</code>
		 */
		FIELD_NE_NOCASE(1),
		/**
		 * In list no case <code>UPPER(FIELD) IN (UPPER(&#63;),UPPER(&#63;),...)</code>
		 */
		IN_LIST_NOCASE(-1),
		/**
		 * Between no case <code>UPPER(FIELD) BETWEEN UPPER(&#63;) AND UPPER(&#63;)</code>
		 */
		BETWEEN_NOCASE(2),
		/**
		 * Does not start with no case, in SQL <code>UPPER(FIELD) NOT LIKE '....&#37;'</code>
		 */
		NOT_LIKE_LEFT_NOCASE(1),
		/**
		 * Does not contain no case, in SQL <code>UPPER(FIELD) NOT LIKE '&#37;....&#37;'</code>
		 */
		NOT_LIKE_MID_NOCASE(1),
		/**
		 * Does not end with no case, in SQL <code>UPPER(FIELD) NOT LIKE '&#37;....'</code>
		 */
		NOT_LIKE_RIGHT_NOCASE(1),
		/**
		 * Not in list no case, in SQL <code>UPPER(FIELD) NOT IN (UPPER(&#63;),UPPER(&#63;),...)</code>
		 */
		NOT_IN_LIST_NOCASE(-1),
		/**
		 * Not between no case, in SQL <code>UPPER(FIELD) NOT BETWEEN UPPER(&#63;) AND UPPER(&#63;)</code>
		 */
		NOT_BETWEEN_NOCASE(2);

		/**
		 * This operator required number of values: -1 more than one, 1 one, 2 two, 0 none.
		 */
		private int requiredValues = -1;

		/**
		 * Constructor assigning the number of required values.
		 *
		 * @param requiredValues
		 */
		Operator(int requiredValues) {
			this.requiredValues = requiredValues;
		}

		/**
		 * Returns the number of required values in the right operand.
		 *
		 * @return The number of required values.
		 */
		public int getRequiredValues() {
			return requiredValues;
		}

		/**
		 * Check if this operator is a NO CASE operator.
		 *
		 * @return A boolean
		 */
		public boolean isNoCase() {
			return toString().contains("NOCASE");
		}
	}

	/**
	 * Validates field, operator and values to construct a valid condition.
	 *
	 * @param field The field or left operand.
	 * @param operator The operator.
	 * @param values The possible list of values as right operand.
	 */
	public final static void validate(Field field, Operator operator, List<Value> values) {
		// The field can not be null.
		if (field == null) {
			throw new NullPointerException("Field can not be null");
		}
		// Null or empty values can only apply to null or not null operators.
		if (values == null || values.isEmpty()) {
			if (!operator.equals(Operator.IS_NULL)
					&& !operator.equals(Operator.NOT_IS_NULL)) {
				throw new IllegalArgumentException("Right operand is expected for operator " + operator);
			}
		}
		// The type of the right operand must be compatible with the type of the field.
		final Types fieldType = field.getType();
		for (Value value : values) {
			if (fieldType.isBoolean() && !value.isBoolean()) {
				throw new IllegalArgumentException("Invalid value type (" + value.getType() + ") for field " + field);
			}
			if (fieldType.isString() && !value.isString()) {
				throw new IllegalArgumentException("Invalid value type (" + value.getType() + ") for field " + field);
			}
			if (fieldType.isNumber() && !value.isNumber()) {
				throw new IllegalArgumentException("Invalid value type (" + value.getType() + ") for field " + field);
			}
			if (fieldType.isDateTimeOrTimestamp() && !value.isDateTimeOrTimestamp()) {
				throw new IllegalArgumentException("Invalid value type (" + value.getType() + ") for field " + field);
			}
			if (fieldType.isByteArray() && !value.isByteArray()) {
				throw new IllegalArgumentException("Invalid value type (" + value.getType() + ") for field " + field);
			}
		}
		// A no case operator requires a field and a value of type string.
		if (operator.isNoCase()) {
			if (!fieldType.isString()) {
				throw new IllegalArgumentException("No case only applies to string fields and values");
			}
			for (Value value : values) {
				if (!value.isString()) {
					throw new IllegalArgumentException("No case only applies to string fields and values");
				}
			}
		}
		// The number of values in the right operand.
		if (operator.getRequiredValues() == -1) {
			if (values == null || values.isEmpty()) {
				throw new IllegalArgumentException("Invañlid number of values for operator " + operator);
			}
		}
		if (operator.getRequiredValues() == 0) {
			if (values != null && values.size() > 0) {
				throw new IllegalArgumentException("Invañlid number of values for operator " + operator);
			}
		}
		if (operator.getRequiredValues() == 1) {
			if (values == null || values.size() != 1) {
				throw new IllegalArgumentException("Invañlid number of values for operator " + operator);
			}
		}
		if (operator.getRequiredValues() == 2) {
			if (values == null || values.size() != 2) {
				throw new IllegalArgumentException("Invañlid number of values for operator " + operator);
			}
		}
	}

	/**
	 * Creates a LIKE_LEFT condition.
	 *
	 * @param field The field to be checked.
	 * @param value The value to check.
	 * @return The condition.
	 */
	public static Condition likeLeft(Field field, Value value) {
		return new Condition(field, Operator.LIKE_LEFT, value);
	}

	/**
	 * Creates a LIKE_LEFT_NOCASE condition.
	 *
	 * @param field The field to be checked.
	 * @param value The value to check.
	 * @return The condition.
	 */
	public static Condition likeLeftNoCase(Field field, Value value) {
		return new Condition(field, Operator.LIKE_LEFT_NOCASE, value);
	}

	/**
	 * Creates a NOT_LIKE_LEFT_NOCASE condition.
	 *
	 * @param field The field to be checked.
	 * @param value The value to check.
	 * @return The condition.
	 */
	public static Condition notLikeLeftNoCase(Field field, Value value) {
		return new Condition(field, Operator.NOT_LIKE_LEFT_NOCASE, value);
	}

	/**
	 * Creates a LIKE_MID condition.
	 *
	 * @param field The field to be checked.
	 * @param value The value to check.
	 * @return The condition.
	 */
	public static Condition likeMid(Field field, Value value) {
		return new Condition(field, Operator.LIKE_MID, value);
	}

	/**
	 * Creates a LIKE_MID_NOCASE condition.
	 *
	 * @param field The field to be checked.
	 * @param value The value to check.
	 * @return The condition.
	 */
	public static Condition likeMidNoCase(Field field, Value value) {
		return new Condition(field, Operator.LIKE_MID_NOCASE, value);
	}

	/**
	 * Creates a NOT_LIKE_MID condition.
	 *
	 * @param field The field to be checked.
	 * @param value The value to check.
	 * @return The condition.
	 */
	public static Condition notLikeMid(Field field, Value value) {
		return new Condition(field, Operator.NOT_LIKE_MID, value);
	}

	/**
	 * Creates a NOT_LIKE_MID_NOCASE condition.
	 *
	 * @param field The field to be checked.
	 * @param value The value to check.
	 * @return The condition.
	 */
	public static Condition notLikeMidNoCased(Field field, Value value) {
		return new Condition(field, Operator.NOT_LIKE_MID_NOCASE, value);
	}

	/**
	 * Creates a LIKE_RIGHT condition.
	 *
	 * @param field The field to be checked.
	 * @param value The value to check.
	 * @return The condition.
	 */
	public static Condition likeRight(Field field, Value value) {
		return new Condition(field, Operator.LIKE_RIGHT, value);
	}

	/**
	 * Creates a LIKE_RIGHT_NOCASE condition.
	 *
	 * @param field The field to be checked.
	 * @param value The value to check.
	 * @return The condition.
	 */
	public static Condition likeRightNoCase(Field field, Value value) {
		return new Condition(field, Operator.LIKE_RIGHT_NOCASE, value);
	}

	/**
	 * Creates a NOT_LIKE_RIGHT condition.
	 *
	 * @param field The field to be checked.
	 * @param value The value to check.
	 * @return The condition.
	 */
	public static Condition notLikeRight(Field field, Value value) {
		return new Condition(field, Operator.NOT_LIKE_RIGHT, value);
	}

	/**
	 * Creates a NOT_LIKE_RIGHT_NOCASE condition.
	 *
	 * @param field The field to be checked.
	 * @param value The value to check.
	 * @return The condition.
	 */
	public static Condition notLikeRightNoCase(Field field, Value value) {
		return new Condition(field, Operator.NOT_LIKE_RIGHT_NOCASE, value);
	}

	/**
	 * Creates a FIELD_EQ condition.
	 *
	 * @param field The field to be checked.
	 * @param value The value to check.
	 * @return The condition.
	 */
	public static Condition fieldEQ(Field field, Value value) {
		return new Condition(field, Operator.FIELD_EQ, value);
	}

	/**
	 * Creates a FIELD_GT condition.
	 *
	 * @param field The field to be checked.
	 * @param value The value to check.
	 * @return The condition.
	 */
	public static Condition fieldGT(Field field, Value value) {
		return new Condition(field, Operator.FIELD_GT, value);
	}

	/**
	 * Creates a FIELD_GE condition.
	 *
	 * @param field The field to be checked.
	 * @param value The value to check.
	 * @return The condition.
	 */
	public static Condition fieldGE(Field field, Value value) {
		return new Condition(field, Operator.FIELD_GE, value);
	}

	/**
	 * Creates a FIELD_LT condition.
	 *
	 * @param field The field to be checked.
	 * @param value The value to check.
	 * @return The condition.
	 */
	public static Condition fieldLT(Field field, Value value) {
		return new Condition(field, Operator.FIELD_LT, value);
	}

	/**
	 * Creates a FIELD_LEcondition.
	 *
	 * @param field The field to be checked.
	 * @param value The value to check.
	 * @return The condition.
	 */
	public static Condition fieldLE(Field field, Value value) {
		return new Condition(field, Operator.FIELD_LE, value);
	}

	/**
	 * Creates a FIELD_NE condition.
	 *
	 * @param field The field to be checked.
	 * @param value The value to check.
	 * @return The condition.
	 */
	public static Condition fieldNE(Field field, Value value) {
		return new Condition(field, Operator.FIELD_NE, value);
	}

	/**
	 * Creates a FIELD_EQ_NOCASE condition.
	 *
	 * @param field The field to be checked.
	 * @param value The value to check.
	 * @return The condition.
	 */
	public static Condition fieldEQNoCase(Field field, Value value) {
		return new Condition(field, Operator.FIELD_EQ_NOCASE, value);
	}

	/**
	 * Creates a FIELD_GT_NOCASE condition.
	 *
	 * @param field The field to be checked.
	 * @param value The value to check.
	 * @return The condition.
	 */
	public static Condition fieldGTNoCase(Field field, Value value) {
		return new Condition(field, Operator.FIELD_GT_NOCASE, value);
	}

	/**
	 * Creates a FIELD_GE_NOCASE condition.
	 *
	 * @param field The field to be checked.
	 * @param value The value to check.
	 * @return The condition.
	 */
	public static Condition fieldGENoCase(Field field, Value value) {
		return new Condition(field, Operator.FIELD_GE_NOCASE, value);
	}

	/**
	 * Creates a FIELD_LT_NOCASE condition.
	 *
	 * @param field The field to be checked.
	 * @param value The value to check.
	 * @return The condition.
	 */
	public static Condition fieldLTNoCase(Field field, Value value) {
		return new Condition(field, Operator.FIELD_LT_NOCASE, value);
	}

	/**
	 * Creates a FIELD_LE_NOCASE condition.
	 *
	 * @param field The field to be checked.
	 * @param value The value to check.
	 * @return The condition.
	 */
	public static Condition fieldLENoCase(Field field, Value value) {
		return new Condition(field, Operator.FIELD_LE_NOCASE, value);
	}

	/**
	 * Creates a FIELD_NE_NOCASE condition.
	 *
	 * @param field The field to be checked.
	 * @param value The value to check.
	 * @return The condition.
	 */
	public static Condition fieldNENoCase(Field field, Value value) {
		return new Condition(field, Operator.FIELD_NE_NOCASE, value);
	}

	/**
	 * Creates a IN_LIST condition.
	 *
	 * @param field The field to be checked.
	 * @param values The values to check.
	 * @return The condition.
	 */
	public static Condition inList(Field field, Value... values) {
		return new Condition(field, Operator.IN_LIST, values);
	}

	/**
	 * Creates a IN_LIST condition.
	 *
	 * @param field The field to be checked.
	 * @param values The values to check.
	 * @return The condition.
	 */
	public static Condition inList(Field field, List<Value> values) {
		return new Condition(field, Operator.IN_LIST, values);
	}

	/**
	 * Creates a NOT_IN_LIST condition.
	 *
	 * @param field The field to be checked.
	 * @param values The values to check.
	 * @return The condition.
	 */
	public static Condition notInList(Field field, Value... values) {
		return new Condition(field, Operator.NOT_IN_LIST, values);
	}

	/**
	 * Creates a NOT_IN_LIST condition.
	 *
	 * @param field The field to be checked.
	 * @param values The values to check.
	 * @return The condition.
	 */
	public static Condition notInList(Field field, List<Value> values) {
		return new Condition(field, Operator.NOT_IN_LIST, values);
	}

	/**
	 * Creates a IN_LIST_NOCASE condition.
	 *
	 * @param field The field to be checked.
	 * @param values The values to check.
	 * @return The condition.
	 */
	public static Condition inListNoCase(Field field, Value... values) {
		return new Condition(field, Operator.IN_LIST_NOCASE, values);
	}

	/**
	 * Creates a IN_LIST_NOCASE condition.
	 *
	 * @param field The field to be checked.
	 * @param values The values to check.
	 * @return The condition.
	 */
	public static Condition inListNoCase(Field field, List<Value> values) {
		return new Condition(field, Operator.IN_LIST_NOCASE, values);
	}

	/**
	 * Creates a NOT_IN_LIST_NOCASE condition.
	 *
	 * @param field The field to be checked.
	 * @param values The values to check.
	 * @return The condition.
	 */
	public static Condition notInListNoCase(Field field, Value... values) {
		return new Condition(field, Operator.NOT_IN_LIST_NOCASE, values);
	}

	/**
	 * Creates a NOT_IN_LIST_NOCASE condition.
	 *
	 * @param field The field to be checked.
	 * @param values The values to check.
	 * @return The condition.
	 */
	public static Condition notInListNoCase(Field field, List<Value> values) {
		return new Condition(field, Operator.NOT_IN_LIST_NOCASE, values);
	}

	/**
	 * Creates a IS_NULL condition.
	 *
	 * @param field The field to be checked.
	 * @return The condition.
	 */
	public static Condition isNull(Field field) {
		return new Condition(field, Operator.IS_NULL, new ArrayList<>());
	}

	/**
	 * Creates a NOT_IS_NULL condition.
	 *
	 * @param field The field to be checked.
	 * @return The condition.
	 */
	public static Condition isNotNull(Field field) {
		return new Condition(field, Operator.NOT_IS_NULL, new ArrayList<>());
	}

	/**
	 * Creates a BETWEEN condition.
	 *
	 * @param field The field to be checked.
	 * @param value1 The first value to check.
	 * @param value2 The last value to check.
	 * @return The condition.
	 */
	public static Condition between(Field field, Value value1, Value value2) {
		return new Condition(field, Operator.BETWEEN, new Value[]{value1, value2});
	}

	/**
	 * Creates a NOT_BETWEEN condition.
	 *
	 * @param field The field to be checked.
	 * @param value1 The first value to check.
	 * @param value2 The last value to check.
	 * @return The condition.
	 */
	public static Condition notBetween(Field field, Value value1, Value value2) {
		return new Condition(field, Operator.NOT_BETWEEN, new Value[]{value1, value2});
	}

	/**
	 * Creates a BETWEEN_NOCASE condition.
	 *
	 * @param field The field to be checked.
	 * @param value1 The first value to check.
	 * @param value2 The last value to check.
	 * @return The condition.
	 */
	public static Condition betweenNoCase(Field field, Value value1, Value value2) {
		return new Condition(field, Operator.BETWEEN_NOCASE, new Value[]{value1, value2});
	}

	/**
	 * Creates a NOT_BETWEEN_NOCASE condition.
	 *
	 * @param field The field to be checked.
	 * @param value1 The first value to check.
	 * @param value2 The last value to check.
	 * @return The condition.
	 */
	public static Condition notBetweenNoCase(Field field, Value value1, Value value2) {
		return new Condition(field, Operator.NOT_BETWEEN_NOCASE, new Value[]{value1, value2});
	}

	/**
	 * The field to compare is the left operand.
	 */
	private Field field = null;
	/**
	 * The operator to apply.
	 */
	private Operator operator = null;
	/**
	 * The right operant is a list of one or more values.
	 */
	private final List<Value> values = new ArrayList<>();

	/**
	 * Generic constructor.
	 *
	 * @param field The field or left operand
	 * @param operator The operator
	 * @param value The value that is the right operand.
	 */
	public Condition(Field field, Operator operator, Value value) {
		this.field = field;
		this.operator = operator;
		this.values.add(value);
		validate(field, operator, values);
	}

	/**
	 * Generic constructor.
	 *
	 * @param field The field or left operand
	 * @param operator The operator
	 * @param values The value or values that are the right operand.
	 */
	public Condition(Field field, Operator operator, List<Value> values) {
		this.field = field;
		this.operator = operator;
		this.values.addAll(values);
		validate(field, operator, values);
	}

	/**
	 * Generic constructor.
	 *
	 * @param field The field or left operand
	 * @param operator The operator
	 * @param values The value or values that are the right operand.
	 */
	public Condition(Field field, Operator operator, Value... values) {
		this.field = field;
		this.operator = operator;
		this.values.addAll(Arrays.asList(values));
		validate(field, operator, this.values);
	}

	/**
	 * Returns the field or left operand.
	 *
	 * @return The field.
	 */
	public Field getField() {
		return field;
	}

	/**
	 * Returns the operator.
	 *
	 * @return The operator
	 */
	public Operator getOperator() {
		return operator;
	}

	/**
	 * Returns the list of values or right operand.
	 *
	 * @return The list of values.
	 */
	public List<Value> getValues() {
		return values;
	}

	/**
	 * Indicates whether some other object is "equal to" this one.
	 *
	 * @return A boolean.
	 * @param o The object to compare with.
	 */
	@Override
	@SuppressWarnings("unchecked")
	public boolean equals(Object o) {
		if (o == null) {
			return false;
		}
		if (getClass() != o.getClass()) {
			return false;
		}
		final Condition cmp = (Condition) o;
		if (!field.equals(cmp.field)) {
			return false;
		}
		if (!operator.equals(cmp.operator)) {
			return false;
		}
		if (!values.equals(cmp.values)) {
			return false;
		}
		return true;
	}

	/**
	 * Returns the hash code value for this List.
	 *
	 * @return The hash code
	 */
	@Override
	public int hashCode() {
		int hash = 0;
		hash ^= field.hashCode();
		hash ^= operator.hashCode();
		hash ^= values.hashCode();
		return hash;
	}
}
